03 · Plan-and-Execute:先谋后动
前置:01 ReAct 的 4.6 节(messages 如何增长)。本篇所有问题都源于那里。
一、问题:任务一长,Agent 就自己收工了
给 ReAct Agent 派活:「把项目里所有旧版日志库调用改成新版,改完跑测试。」
第 1 步 搜索 → 找到 12 处
第 2-8 步 逐个改 → 改完 6 个
第 9 步 第 7 个文件还依赖了旧版专有方法
第 10 步 查新版替代
第 11 步 修好这个文件
第 12 步 「已完成日志库迁移。」 ← 还剩 5 个文件,测试一次没跑
成因不是模型笨,是上下文结构。 每轮调模型都要重发完整历史,此时历史是:
| 位置 | 内容 |
|---|---|
| 最前面 | 「12 处 + 跑测试」这个全局目标 |
| 中段 | 11 轮工具调用,上万 token 的文件内容 |
| 末尾 | 刚修好第 7 个文件 |
全局目标和「刚做完的事」分处两端,中间隔着注意力最弱的区域。 模型据以判断是否完成的,是右边那块。
模型注意力在长上下文中段最弱(lost in the middle),最清楚的是末尾。刚修完一个文件、感觉完整,就收工了。
ReAct 的结构缺陷:只有「下一步」,没有「全局」。
两个朴素解法都不够用:
- 在 system prompt 里强调目标 —— 它是静态的,记不住「已经改了几个」
- 让模型先列个计划 —— 方向对,但计划列完放哪?打印出来模型下轮看不见;塞进 system prompt 就改不了,而第 9 步恰好证明计划一定要改
所以这个范式真正要解决的是两件事:① 全局目标怎么始终停在模型看得见的地方;② 计划中途怎么改。
二、两条路线
| ① 静态计划 | ② 动态 todo | |
|---|---|---|
| 计划何时定 | 开头一次 | 随时改 |
| 存在哪 | 一个 Python 变量 | Agent 状态 + 上下文 |
| 谁维护 | 你的编排代码 | 模型自己,通过工具调用 |
| 代表 | Plan-and-Solve(arXiv 2305.04091) | Claude Code TodoWrite、Manus todo.md、LangChain write_todos |
| 适合 | 步骤能提前想清的推理题 | 边做边发现的工程任务 |
生产环境跑的基本都是② —— 真实任务的计划一定会变。但①值得先看,因为它把「执行器需要哪些信息」暴露得最清楚。
① 静态计划:规划器出完计划即退场,无回边
② 动态 todo:改计划本身就是循环里的一次工具调用
三、静态计划:执行器需要四样东西

图 3-1 静态计划的两阶段
图片来源:Hello-Agents 第四章(CC BY-NC-SA 4.0)
规划器没什么可讲的 —— 一次普通调用,要它输出步骤列表。重点在执行器:执行第 3 步时要告诉模型什么?
EXECUTOR_PROMPT = """严格按计划解决"当前步骤",只输出该步答案。
# 原始问题: {question}
# 完整计划: {plan}
# 历史步骤与结果: {history}
# 当前步骤: {current_step}
"""
四个占位符,各对应一种翻车:
| 占位符 | 不给会怎样 |
|---|---|
question | 忘了最终目标,把第 3 步当孤立小任务做 |
plan | 不知道当前步的位置,越界去干第 4 步 |
history | 拿不到前面算出的中间值,只能瞎猜 |
current_step | 试图一次把整个问题解完 |
两个死穴,正是路线②要解决的:
history全量累加 —— 每步都要带上之前所有步骤,步骤数翻倍则 token 翻四倍- 计划改不了 —— 第 2 步发现原计划有误,只能硬走或整个重来
针对死穴 1 有两个变体:ReWOO 在规划期就用
#E1、#E2写清步骤间的变量依赖,执行时不带历史;LLM Compiler 把计划编译成 DAG 并行跑。两者都假设「计划不会变」,所以生产上少见。
四、动态 todo:把计划做成一个工具
不在 ReAct 外面套规划阶段,而是给 ReAct 加一个「写待办清单」的工具。改计划本身就是一次工具调用,于是计划随时可改、且以工具结果的形式进入消息历史。
4.1 数据结构:三个状态,故意缺一个
class Todo(TypedDict):
content: str
status: Literal["pending", "in_progress", "completed"]
没有优先级、ID、依赖、子任务,也没有 failed。
缺 failed 是刻意的。 有这个状态,模型遇到搞不定的任务就会标记 failed 然后跳过;而你要的是它去解决卡住的原因。官方规定:
遇到错误、阻塞或无法完成时保持
in_progress;被阻塞时新建一个任务描述需要解决什么。
用状态机里缺一个状态来约束模型行为。
4.2 34% 的文件是提示词
实测 langchain/agents/middleware/todo.py:
| 字符数 | |
|---|---|
| 整个文件 | 15,525 |
| 工具描述 | 3,881 |
| 系统提示词 | 1,378 |
| 提示词占比 | 34% |
Python 逻辑不到 60 行,工具本体只有 10 行(todos 全量覆盖,不是增量更新)。
这是本篇的认知转折点:01、02 的行为由代码结构决定,Plan-and-Execute 的行为几乎全由提示词决定。删掉 write_todos 的描述,工具还在,模型不会用了。
4.3 提示词里真正改变行为的四条
① 反复劝你别用
如果用户的请求很简单、不到 3 步,最好不要用,直接做。
开头和结尾各说一遍。写清单要烧 token 和延迟,小任务上是纯负收益。
② 立刻标记,禁止批量标
批量标记会让上下文里长时间存在一份过期清单,模型看到「都还是 pending」可能重做已完成的事。
③ 永远至少有一个 in_progress
给模型一个「当前焦点」锚点。全是 pending 时模型不知道自己在哪。
④ 标记完成 ≠ 交付答案
write_todos追踪工作,它不交付答案。用户要的东西必须出现在最后一次write_todos之后的消息里。
对应一个真实翻车场景:
第 9 轮 调 write_todos,全部标记 completed
第 10 轮 模型觉得没事干了,不调工具 → 循环退出 → 用户屏幕上什么都没有
因为 01 讲过,退出条件是「这轮没有工具调用」,而它最后一个动作恰好是调工具。
4.4 实测发现:清单其实没被回注上下文
wrap_model_call 钩子只往 system prompt 追加了使用说明,没有注入当前 todos 状态。全文件搜一遍:todos 只写不读。模型看到清单的唯一途径是历史里那条工具返回消息:
Updated todo list to [{'content': '搜索日志调用', 'status': 'completed'}, ...]
所以对话越长,清单越往中段沉 —— 正好落进第一节那个注意力低谷。
对比 Manus 的做法:每完成一步就把 todo.md 重写一遍追加到上下文末尾。 表面在更新文件,实际是把全局目标反复搬到注意力最强的位置。
| LangChain | Manus | |
|---|---|---|
| todo 的定位 | 给用户看的进度条 | 对抗注意力衰减的工具 |
| 用户可见 | 主要目的 | 副作用 |
自己实现建议照 Manus 做,至少在每次调模型前把清单注入 system prompt 末尾。
LangChain:清单只在生成那一刻写进历史,此后不断下沉
Manus:每完成一步重写一次,清单永远贴着当前位置
对照第一节那张注意力图:LangChain 的清单会滑进灰色区域,Manus 的清单永远待在绿色区域。
五、故障排查
| 现象 | 原因 | 修法 |
|---|---|---|
| 列了清单然后就停了 | 最后动作是调工具,循环判定结束 | 4.3 ④ |
| 清单写得好但没照着做 | 清单沉到上下文中段 | 每轮回注末尾(4.4) |
| 看个变量类型也列 5 条 todo | 缺劝阻性提示词 | 明写「不到 3 步别用」 |
某任务永远 in_progress | 模型不知道卡住了该干嘛 | 要求「被阻塞时新建解决阻塞的任务」 |
| 清单错乱互相覆盖 | 一轮并行调了两次,全量覆盖竞态 | 拦截并行调用,回错误让模型重试 |
| 第 10 步 prompt 三万 token | 静态计划全量 history | 换动态 todo,或摘要压缩 |
最后那条竞态,生产实现专门写了段代码拦,注释是:
清单被设计为每轮最多更新一次。
write_todos每次替换整个清单,多次并行调用会产生「哪次优先」的歧义。
处理方式不是抛异常,是构造错误消息塞回历史,让模型看到「并行调用被拒」,下轮自己改成单次。给模型可读懂、可改正的错误 —— 这条会在 05、06 反复出现。
六、定位与边界
Plan-and-Execute 是在 ReAct 之上加一层全局视野,没有取代 ReAct —— 动态 todo 本身就跑在 ReAct 循环里。
不该用:任务不到 3 步(官方自己劝你别用);路径完全确定(那是 Workflow);探索型任务(目标都不清楚,硬列计划反而限制它);首字延迟敏感(静态路线的规划阶段是一次完整往返)。
一句话:这个范式的关键不是「把计划列出来」,而是「让模型每轮都还能看见它」。
参考资料
| 资料 | 位置 | 协议 |
|---|---|---|
| 生产实现(主拆) | langchain/libs/langchain_v1/langchain/agents/middleware/todo.py,357 行 | MIT |
| 教学实现 | s05_todo_write/code.py,13KB | MIT |
| 静态计划路线 | Hello-Agents 第四章 4.3 | CC BY-NC-SA 4.0 |
| 官方教程 | langgraph/examples/plan-and-execute/plan-and-execute.ipynb | MIT |
| 为什么 todo 有效 | Manus — Context Engineering for AI Agents | —— |
Wang L, Xu W, Lan Y, et al. Plan-and-Solve Prompting. arXiv:2305.04091, 2023.
下一篇:04 · Reflection